﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Heartbeat
{
    public delegate void InternalMessageCallbackType(Packet pPacket);
    public delegate void InternalReadyCallbackType();
    public class TcpConnection
    {
        protected Socket mSocket = null;
        protected String mAddress;
        protected const int HEADER_LENGTH = 4;
        protected const int CHANNEL_LENGTH = 4;
        protected InternalMessageCallbackType mInternalOnMessageCallback = null;
        protected InternalReadyCallbackType mInternalOnReadyCallback = null;

        public TcpConnection()
        {
            mSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        public void Start(string address, int port)
        {
            mAddress = address;
            mSocket.BeginConnect(address, port,
                new AsyncCallback(ConnectCallback), mSocket);
        }

        public void Close()
        {
            mSocket.BeginDisconnect(false,
                new AsyncCallback(CloseCallback), null);
        }

        public void SetInternalOnMessageCallback(InternalMessageCallbackType cb)
        {
            mInternalOnMessageCallback = cb;
        }

        public void SetInternalOnReadyCallback(InternalReadyCallbackType cb)
        {
            mInternalOnReadyCallback = cb;
        }

        public virtual void Write(Packet pPacket)
        {
            string msgStr = pPacket.GetMessage().ToString(Formatting.None);

            byte[] msgByteArr = Encoding.UTF8.GetBytes(msgStr);
            byte[] lenByteArr = BitConverter.GetBytes(msgStr.Length);
            byte[] channelByteArr = BitConverter.GetBytes(pPacket.GetChannel());
            byte[] byteData = new byte[msgStr.Length + HEADER_LENGTH + CHANNEL_LENGTH];
            System.Buffer.BlockCopy(lenByteArr, 0, byteData, 0, HEADER_LENGTH);
            System.Buffer.BlockCopy(channelByteArr, 0, byteData, HEADER_LENGTH, CHANNEL_LENGTH);
            System.Buffer.BlockCopy(msgByteArr, 0, byteData, HEADER_LENGTH + CHANNEL_LENGTH, msgStr.Length);
            if (!BitConverter.IsLittleEndian)
                Array.Reverse(byteData);
            mSocket.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(SendCallback), mSocket);
        }

        protected virtual void AsyncReceiveBody(int bodyLength, int channel)
        {
            //Create a new state object for the body.
            StateObject bodyState = new StateObject(bodyLength);
            bodyState.channel = channel;

            //Begin receiving the data.
            mSocket.BeginReceive(bodyState.mBuffer, 0, bodyState.mBufferSize, 0,
                new AsyncCallback(ReceiveBodyCallback), bodyState);
        }

        protected virtual void AsyncReceiveHeader()
        {
            try
            {
                // Create the state object.
                StateObject state = new StateObject(HEADER_LENGTH + CHANNEL_LENGTH);

                // Begin receiving the data from the remote device.
                mSocket.BeginReceive(state.mBuffer, 0, state.mBufferSize, 0,
                    new AsyncCallback(ReceiveHeaderCallback), state);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        protected virtual void StartReading()
        {
            AsyncReceiveHeader();
        }

        protected virtual void CloseCallback(IAsyncResult ar)
        {
            //mSocket.Close();
        }

        protected virtual void ConnectCallback(IAsyncResult ar)
        {
            mSocket.EndConnect(ar);

            OnConnected();
        }

        protected virtual void OnConnected()
        {
            OnReady();
        }

        protected void OnReady()
        {
            mInternalOnReadyCallback();

            StartReading();
        }

        protected virtual void ReceiveBodyCallback(IAsyncResult ar)
        {
            try
            {
                //Retrieve the state object and the client socket 
                //from the asynchronous state object.
                StateObject state = (StateObject)ar.AsyncState;

                //Read data from the remote device.
                int bytesRead = mSocket.EndReceive(ar);
                if (bytesRead <= 0)
                    return;

                //Store the data received so far.
                state.mSb.Append(Encoding.UTF8.GetString(state.mBuffer, 0, bytesRead));

                //Reduce the size of the buffer by the bytes read.
                state.mBufferSize -= bytesRead;

                //If we're out of bytes to read, body is complete and we should handle the message.
                if (state.mBufferSize <= 0)
                {
                    MessageReceived(state.mSb.ToString(), state.channel);

                    //Start reading for the next header.
                    StartReading();
                }
                else
                {
                    //Get the rest of the data.
                    mSocket.BeginReceive(state.mBuffer, 0, state.mBufferSize, 0,
                        new AsyncCallback(ReceiveHeaderCallback), state);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        protected void MessageReceived(string message)
        {
            JObject msgJson = JObject.Parse(message);
            JToken headerJson = msgJson["header"];
            int channel = (int)(headerJson["channel"]);
            MessageReceived(new Packet(channel, (JObject)msgJson["message"]));
        }

        protected void MessageReceived(string message, int channel)
        {
            Packet pPacket = new Packet(channel, JObject.Parse(message));
            MessageReceived(pPacket);
        }

        protected void MessageReceived(Packet pPacket)
        {
            mInternalOnMessageCallback(pPacket);
        }

        protected virtual void ReceiveHeaderCallback(IAsyncResult ar)
        {
            try
            {
                //Retrieve the state object and the client socket 
                //from the asynchronous state object.
                StateObject state = (StateObject)ar.AsyncState;

                //Read data from the remote device.
                int bytesRead = mSocket.EndReceive(ar);
                if (bytesRead <= 0)
                    return;

                //Store the data received so far.
                state.mSb.Append(Encoding.ASCII.GetString(state.mBuffer, 0, bytesRead));

                //Reduce the size of the buffer by the bytes read.
                state.mBufferSize -= bytesRead;

                //If we're out of bytes to read, the header is complete and we need to read the body.
                if (state.mBufferSize <= 0)
                {
                    byte[] byteArr = state.mBuffer;
                    if (!BitConverter.IsLittleEndian)
                        Array.Reverse(byteArr);

                    int bodyLength = BitConverter.ToInt32(byteArr, 0);
                    int channel = BitConverter.ToInt32(byteArr, HEADER_LENGTH);

                    //Start the body read.
                    AsyncReceiveBody(bodyLength, channel);
                }
                else
                {
                    //Get the rest of the data.
                    mSocket.BeginReceive(state.mBuffer, 0, state.mBufferSize, 0,
                        new AsyncCallback(ReceiveHeaderCallback), state);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        protected virtual void SendCallback(IAsyncResult ar)
        {
            mSocket.EndSend(ar);
        }
    }
}
